Глава 12.

Управление исключениями

Грамотно организованная, устойчивая программа должна справляться с нестандартными ситуациями, встречающимися в реальной работе с реальными данными. Такой нестандартной ситуацией может быть, например, ошибка пользователя при вводе данных или нарушение структуры некоторого файла. В языках, предшествующих C++, подобные проблемы решались с помощью глобальных “флагов ошибки” или приписыванием определенному значению, возвращаемому функцией, специального смысла “индикатора ошибки”. Язык C++ вводит понятие управления исключениями, т. е. специальных средств изменения программного потока управления с целью обработки нестандартных, непредвиденных или ошибочных ситуаций, возникающих в процессе работы.

В главе 4 мы вкратце упоминали о т. н. структурированной обработке исключений (SEH) в языке С; она реализуется в C++ Builder при посредстве нестандартных ключевых слов _try, _except и _finally. Ее принципы заимствованы, на самом деле, из операционной системы Windows NT; в NT это средства, встроенные в систему.

В C++ средства обработки исключений встроены непосредственно в язык. Ключевые слова, связанные с данным аспектом языка, следующие: try, catch и throw.

Из достоинств обработки ошибок с использованием исключений, по сравнению с традиционными методами, можно назвать следующие:

Недостаток обработки исключений состоит, на наш взгляд, в том, что приходится вводить дополнительные уровни операторных скобок; код делается более громоздким. Но это в любом случае с лихвой компенсируется ее достоинствами.

Следует заметить, что, в отличие от модели SEH, при генерировании исключения в C++ невозможно продолжить выполнение программы с того самого места, где оно возникло.

Основные синтаксические конструкции

Общий синтаксис обработки исключения в C++ такой:

try { // Начало "пробного блока".

throw выражение; / / "Выбрасывание" исключения.

} catch(тип переменная) { // Заголовок обработчика для <типа>.

тело_обработчика) [catch ...] // Возможно, обработчики других типов.

Теперь мы в деталях рассмотрим элементы этой конструкции.

Блок try

Ключевое слово try начинает пробный блок операторов, показывая, что данный блок может генерировать исключение. Тело блока заключается в фигурные скобки. Оно может содержать вызовы функций, тело которых при этом тоже будет рассматриваться как принадлежащее пробному блоку. Другими словами, весь код, могущий прямо или косвенно исполняться при входе в блок, принадлежит пробному блоку:

try {

cout << "Входим в пробный блок..."<< end.1;

DangerousFunc(); // Вызов процедуры, способной

// генерировать исключение.

}

// Конец try-блока.

Блоки try могут быть вложенными.

Блок catch

За пробным блоком следует один или несколько обработчиков исключения, начинающихся ключевым словом catch. За ним следует объявление исключения в круглых скобках, аналогичное формальному параметру функции:

try {

}

catch(int. i) { // Перехватывает исключения типа int.

} catch(char* str) { // Перехватывает char*.

} catch (...) { // Перехватывает все остальное.

Если тип выброшенного в пробном блоке исключения совпадает или совместим (об этом позже) с типом в объявлении некоторого обработчика, то данный обработчик перехватывает исключение. Если нет, то поиск подходящего обработчика продолжается далее. Обработчик, в заголовке которого вместо объявления исключения стоит многоточие (...), перехватывает исключения любого типа; такой обработчик должен быть последним в ряду тех, что следуют за данным блоком try.

Если пробный блок не генерировал никакого исключения, управление, по выходе из него, передается первому оператору, следующему за последним из обработчиков исключений.

Оператор throw

Исключения могут генерироваться или, как принято говорить в C++, выбрасываться либо исполнительной системой C++, стандартными функциями и т. д., либо самим программистом с помощью оператора throw. Он состоит из ключевого слова throw, за которым следует необязательное выражение.

Throw с операндом

Выражение, следующее за ключевым словом throw, можно рассматривать как фактический параметр (аргумент) в вызове функции, хотя здесь круглые скобки не обязательны. Обычно это константа или переменная, тип которой может быть любым, как встроенным, так и пользовательским. Тип операнда определяет обработчик, который будет вызван при исполнении оператора throw. На данный момент мы уже знаем достаточно, чтобы посмотреть законченный пример.

Листинг 12.1. Программа, демонстрирующая простейшие исключения

///////////////////////////////////

// SimpTypes.срр: Перехват простых исключений.

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

int main () (

double d = 1.0;

for (int i=0; i<4; i++) { . try {

cout << endl<< "Entering the try-block..." <<end1;

switch (i) { case 0:

throw "Throwing an exception of char*"; // Выбросить

// строку. case 1:

throw 5; // Выбросить

// целое.

default:

throw d; // Выбросить double. }

// Следующий оператор исполняться не будет

// из-за исключений.

cout<< "In the „try-block after all exceptions..." << endl;

} // Конец try-блока.

catch(int 1) { // Обработчик int.

cout << "Int thrown: " << 1 << endl;

} catch(char* str) { // Обработчик char*.

cout << "String thrown: " << str << endl;

} catch (...) { // Для всего остального.

cout << "An unknown type thrown."<< "Program will.terminate." << endl;

cin.ignore () ;

return -1; // Завершить программу. }

cout<< "End of the loop."<< endl;

} // Конец цикла.

cout << "The End." << endl; // Эти операторы не исполняются cin.ignore (); // никогда, т.к. третье

// исключение

return 0; // завершает программу. }

Вывод программы показан на рис. 12.1

Давайте разберемся, что здесь происходит.

В программе организован цикл, который должен выполниться четыре раза. В нем находится пробный блок, генерирующий исключения различных типов — int, char* и double в зависимости от значения счетчика цикла. На первом проходе оператор throw выбрасывает строку, которая перехватывается вторым по счету обработчиком. Так как обработчик не выполняет никаких действий, кроме вывода сообщения, выполнение про-

Рис. 12.1 Простая программа с исключениями

граммы продолжается с оператора, следующего за списком обработчиков. Цикл продолжается, и при втором входе в пробный блок выбрасывается тип int, перехватываемый первым обработчиком.

На третьем проходе цикла выбрасывается переменная типа double, для которого обработчика не предусмотрено. Однако имеется “всеядный” третий обработчик. Он исполняет оператор return, завершающий программу. Поэтому цикл for в четвертый раз не выполняется и вообще программа не доходит до своего “нормального” конца.

Обратите внимание, что последний оператор пробного блока (вывод сообщения) не будет выполняться никогда, так как мы в любом случае выбрасываем исключение в предшествующем ему блоке switch.

Порядок следования catch-обработчиков

Выше мы сказали, что при генерировании исключения ищется обработчик для типа, совпадающего или совместимого с типом исключения. Тут нужна некоторая осторожность. Совместимость в донном случае означает, что:

Процедура поиска не ищет “наилучшего соответствия” типов, а просто берет первый по порядку следования подходящий обработчик. Например, у вас есть два класса исключения, причем второй является производным от первого. Если в списке обработчиков первым будет стоять тот, что предназначен для исключений базового класса, он будет перехватывать все исключения — как базового, так и производного классов. Или рассмотрите такой пример:

int main() {

try {

throw "Throwing char*"; // Выбрасывает char*. }

catch(void*) ( // Ловит void*.

cout<< "Void* caught." << endl;

return -1;

}

catch(char*) { // Ловит char*.

cout << "Char* caught." << endl;

return -1;

}

return 0;

}

Здесь обработчики исключений расположены в неправильном порядке, так как обработчик для void* будет перехватывать все исключения, предназначенные для обработчика char*.

Throw без операнда

Если в операторе throw не указан операнд, то обрабатываемое в данный момент исключение перебрасывается, т. е. поиск подходящих обработчиков будет продолжен далее. Сказанное означает, что такой оператор может применяться только в catch-обработчике или функции, вызываемой из некоторого обработчика.

Спецификации исключений.

В определении функции можно указать, исключения какого типа она может выбрасывать. Спецификация исключений для функции выглядит так:

<тип> FuncName(<список параметров>) throw([<тип>

[, <тип> ...]])

{

<тело функции>

}

Тем самым мы сообщается, что функция может выбрасывать только типы, перечисленные в списке после ключевого слова throw. Если этот список пустой, то функция вообще не должна выбрасывать никаких исключений.

Обработка непредвиденных исключений

Однако то, какие исключения функция прямо или косвенно выбрасывает на самом деле, выясняется только во время выполнения. Компилятор не выдает никаких ошибок или предупреждений на этот счет. Если функция, снабженная спецификацией исключений, выбрасывает непредвиденное, т. е. не указанное в спецификации, исключение, вызывается функция unexpected () . По умолчанию последняя просто вызывает terminate () . Вы можете, тем не менее, указать свою собственную функцию, которая должна активироваться при появлении непредвиденных исключений, вызвав set_unexpected (). Прототип ее находится в файле except.h (не обращайте внимания на _RTLENTRY; он расширяется в _cdecl):

typedef void (_RTLENTRY *unexpected_function)();

unexpected_function _RTLENTRY set_unexpected(unexpected_function);

Возвращается указатель на предыдущую функцию обработки. Процедура для непредвиденных исключений не должна возвращать управление. Она может либо аварийно завершить программу, либо выбросить исключение.

Механика исключений

Следующие параграфы посвящены некоторым механизмам, которые нужно хорошо понимать, чтобы эффективно пользоваться возможностями управления исключениями.

Исключения и стек

Когда выбрасывается исключение, управление передается некоторому обработчику; он должен принадлежать функции, еще находящейся на стеке вызовов программы (см. в главе 5 об окне стека вызовов). Но эта функция не обязательно будет текущей функцией (выбросившей исключение). Например, пробный блок и обработчики находятся в одной функции, а исключение выбрасывается некоторой функцией, вызываемой из пробного ; блока; между ними может тянуться целая цепочка вложенных вызовов.

В результате некоторой последовательности вызовов на стеке (физическом) будут находиться адреса возврата, передаваемые аргументы и локальные переменные вызванных к данному моменту функций, а также вспомогательная информация (формирующая кадры стека), которая используется кодом входа и выхода из функции.

При нормальном возврате из функции она удаляет со стека локальные переменные, вызывая соответствующий деструктор, если переменная — представитель класса. Затем она восстанавливает кадр стека вызывающей функции и исполняет инструкцию возврата. После этого вызывающая функция удаляет со стека передававшиеся аргументы, также вызывая при необходимости деструкторы.

Приведем небольшую иллюстрацию. Ниже показана программа, состоящая из main () и двух функций FuncA () и FuncB () . Главная функция создает объект класса S и передает его FuncA (), которая его модифицирует и передает в FuncB (). Затем управление возвращается к main () .

Листинг 12.2. Работа стека при вызовах функций

///////////////////////////////////

// Stack.срp: Работа стека.

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

struct S // Простой класс. {

int s;

S(int ss): s(ss) // Конструктор (преобразования из int).

{

cout << "Constructor for "<< s << endl;

} S (const S& src) // Конструктор копии.

{

s = src.s;

cout << "Copy constructor for " << s << endl;

}

~S() // Деструктор.

{

cout << "Destructor of " << s << endl;

} };

void FuncB(S obj)

{

cout << "In FuncB: got << obj.s endl;

cout << "Exiting FuncB..." << endl;

}

void FuncA(S obj)

{

cout << "In FuncA: got"<< obj.s << endl;

obj.s = 22; // Модифицирует полученную копию объекта и...

FuncB(obj); // ...передает ее FuncB().

cout << "Exiting FuncA..." << end1;

}

int main() {

S mainObj = 11; // Локальный объект.

cout << "In main..." << endl; FuncA(mainObj);

cout << "Exiting main..." << endl;

return 0;

}

Программа выводит следующие сообщения:

Constructor for 11

In main...

Copy constructor for 11

In FuncA: got 11

Copy constructor for 22

In FuncB: got 22

Exiting FuncB...

Destructor of 22

Exiting FuncA...

Destructor of 22

Exiting main...

Destructor of 11

Здесь видно, как создается копия объекта при передачи параметра (по значению) и как она удаляется при возврате из функции.

При появлении исключения нормального возврата не происходит. Требуемый обработчик может находиться на несколько позиций выше по стеку вызовов. Запускается механизм разматывания стека, удаляющий находящиеся на нем локальные объекты с вызовом деструкторов, как если бы происходил нормальный возврат из функций, находящихся еще на стеке вызовов.

Можно слегка модифицировать предыдущий пример, организовав пробный блок в main() и заставив FuncB() выбрасывать исключение в виде строки:

void FuncB(S obj)

{

cout << "In FuncB: got " << obj.s << endl;

cout << "Throwing exception..." << endl;

throw "Exception!";

cout << "Exiting FuncB..." << endl;

}

int main() {

S mainObj = 11; // Локальный объект.

cout << "In main..." << endl;

try {

FuncA(mainObj);

} catch(char* str) {

cout << "Gaught in main: " << str << end1;

} cout << "Exiting main..." << endl;

return 0;

}

Теперь программа выводит:

Constructor for 11

In main...

Copy constructor for 11

In FuncA: got 11

Copy constructor for 22

In FuncB: got 22

Throwing exception...

Destructor of 22

Destructor of 22

Caught in main: Exception!

Exiting main...

Destructor of 11

Временные копии объекта уничтожаются по-прежнему, хотя возврата из функции в обычном смысле слова не происходит.

Поиск обработчика и неуправляемые исключения

Если не удается найти подходящий обработчик исключения в списке текущего пробного блока, происходит переход на более высокий уровень, т. е. к списку обработчиков try-блока, -непосредственно включающего текущий. Если такой, конечно, имеется.

Если обработчика для данного исключения в программе не находится вообще, оно считается неуправляемым. В этом случае вызывается функция terminate () . Она, в свою очередь, вызывает функцию abort () , которая аварийно завершает программу.

Можно установить свою собственную процедуру завершения с помощью функции set_terminate () ; прототип ее находится в except, h:

typedef void(_RTLENTRY *terminate_function) ();

terminate_function _RTLENTRY set_terminate(terminate function);

Как и другие подобные функции, она возвращает адрес предыдущей процедуры завершения. Процедура завершения не может ни возвратить управление, ни выбросить исключение.

Следующая программа демонстрирует некоторые моменты вышесказанного. Ее вывод показан на рис. 12.2.

Листинг 12.3. Поиск обработчиков и неуправляемые исключения

/////////////////////////////////////

//Unhandled. срр: Прохдедура для неуправляемых исключений.

//

#include <iostream.h>

#include <except.h>

#pragma hdrstop

#include <condefs.h>

class Dummy {}; // Пустой класс исключения.

void FuncB(int f) {

if (!f) { cout << "FuncB: throwing int..." << endl;

throw 7;

} else {

cout<< "FuncB: throwing char*..."<< endl;

throw "Exception!";

} }

void FuncA(int f)

{

try {

FuncB(f);

} catch(char* str) { // Обработчик выбрасывает Dummy.

cout << "FuncA: char* caught. Rethrowing Dummy..."<< endl;

Dummy d;

throw d;

} }

void MyTerminate() // Новая процедура завершения. {

cout << "Termination handler called..." << endl;

abort ();

}

int main() {

set_terminate(MyTerminate); // Установка процедуры

// завершения.

for (int j=0; j<2; j++) { try {

FuncA(j) ;

} catch(int k) {

cout << "Main: int caught - " << k << endl;

} }

// Следующие операторы исполняться не будут... cout “ "Exiting main..." “ endl;

return 0;

}

Рис.12.2 Выполнение программы Unhandled

Тело пробного блока в main () выполняется два раза. Имеется вложенный пробный блок в FuncA () . На первом проходе FuncB () выбрасывает int, для которого нет обработчика во внутреннем блоке и потому перехватываемое во внешнем пробном блоке, т. е. в main О . На втором проходе выбрасывается строка, которая перехватывается в FuncA () . Обработчик сам выбрасывает исключение Dummy — неуправляемое, поэтому вызывается установленная пользователем процедура завершения.

Исключения и классы

Как уже упоминалось, выбрасывать можно не только выражения простых типов, но и объекты классов, определенных пользователем. Кроме того, нужно рассмотреть еще ряд вопросов, связанных с работой конструкторов и деструкторов во взаимодействии с механизмом исключений.

Исключения, конструкторы и деструкторы

Следующие два параграфа посвящены процессам, происходящим при генерировании исключения в конструкторе класса.

Локальные (автоматические) объекты

Когда выброшено исключение, начинается разматывание стека с вызовом необходимых деструкторов. Однако деструкторы в этом случае вызываются только для полностью конструированных локальных объектов. Это означает, что если исключение выброшено в конструкторе объекта, для самого этого объекта деструктор вызван не будет. Будут вызваны только деструкторы его элементов-объектов и базовых классов. Поэтому, если объект содержал уже к этому времени указатели, например, на выделенную динамическую память, она освобождаться не будет. Возникнет утечка памяти.

Рассмотрите такой пример:

Листинг 12.4. Исключение в конструкторе

/////////////////////////////////

// Construct.срр: Исключение в конструкторе. //

#inciude <stdio.h>

#include <stdlib.h>

#include <string.h>

#pragma hdrstop

#include <condefs.h>

void* operator new[](size_t size)

// Глобальная new[].

{

printf("Global new[].\n");

return malloc(size);

}

void operator delete[](void *p) // Глобальная delete[].

{

printf("Global delete[].\n");

free (p) ;

}

class Hold { // Класс, содержащий динамический массив char. char *ptr;

public:

Hold(char *str) // Конструктор преобразования из char*.

{

printf("Constructor.\n") ;

ptr = new char[strlen(str)+1] ;

strcpy(ptr, str) ;

// printf("Constructor: throwing exception...\n");

// throw "Exception!";

} ~Hold() // Деструктор.

{

printf("Destructor.\n") ;

delete [ ] ptr;

}

void Show() // Распечатка строки.

{

printf("My contents: %s\n", ptr);

} };

int main() {

try {

Hold h = "Some string."; // Попытка конструировать

// объект. h.Show() ;

} catch(char *str) {

printf("Message caught: %s\n", str);

}

printf("Exiting main...\n");

return 0;

}

Программа создает локальный в try-блоке объект класса Hold. Строка в конструкторе, выбрасывающая исключение, пока закомментирована, и программа выводит:

Constructor.

Global new[].

My contents: Some string.

Destructor.

Global delete [].

Exiting main...

Вопрос на сообразительность: почему мы для вывода сообщений пользовались в этом примере функцией библиотеки С printf (), а не потоковыми операциями C++?

Если же раскомментировать строку, будет выброшено исключение, причем, поскольку деструктор не полностью конструированного объекта не вызывается, операция delete [ ] для уже выделенной строки выполнена не будет:

Constructor.

Global new[].

Constructor: throwing exception...

Message caught:Exception!

Exiting main...

Отсюда можно сделать полезный методический вывод: ресурсы, подобные выделяемой памяти (например, графические объекты Windows), следует оформлять как классы, которые будут входить в целевой класс в качестве его элементов. Класс из предыдущего примера можно модифицировать примерно так:

class Hold { // Класс, содержащий динамический

// массив char. struct IChar { // Вложенный класс, инкапсулирующий

// массив. char *ptr;

IChar(char *str) {

printf("IChar: constructor.\n");

ptr = new char[strlen(str)+1];

strcpy(ptr, str) ;

}

~IChar() {

printf("IChar: destructor.\n") ;

delete [] ptr;

}

} iStr; // Элемент - объект IChar. public:

Hold(char *str) // Конструктор преобразования из char*.

iStr(str) // Инициализатор элемента iStr. {

printf("Constructor: throwing exception ...\n");

throw "Exception!";

} ~Hold() // Деструктор - ничего не делает.

{

printf("Destructor.\n");

} void Show() // Распечатка строки.

{

printf("My contents: %s\n", iStr.ptr);

} };

Как видите, действия по выделению и освобождению памяти возложены теперь на класс IChar. Он, конечно, не обязан быть вложенным, как я сделал здесь (зачем, и сам не знаю). Программа выводит:

IChar: constructor.

Global new[].

Constructor: throwing exception...

IChar: destructor.

Global delete [].

Message caught: Exception!

Exiting main...

Хотя, как видите, деструктор Hold и не вызывается, деструктор его полностью конструированного элемента iStr вызывается правильно и память освобождается.

Динамические объекты

Если объект создается с помощью операции new своего класса, и в конструкторе класса генерируется исключение, то деструктор класса не вызывается. В этом отношении все происходит совершенно так же, как описано выше для явных вызовов конструктора (т. е. для локальных объектов).

Хотя деструктор не вызывается, память объекта (не та, на которую он может ссылаться посредством указателей, а его собственная) автоматически удаляется. По сути, то же происходит и с локальными объектами, только там не полностью конструированный объект просто удаляется со стека, здесь же вызывается операция класса delete. Вот пример:

Листинг 12.5. Исключение в конструкторе динамического объекта

/////////////////////////////////

// Dynamic.срр: Исключение при операции класса new.

//

#include <iostream.h>

#include <string.h>

#pragma hdrstop

#include <condefs.h>

const int MaxLen = 80;

class AClass {

char msg[MaxLen];

public:

AClass () // Конструктор, выбрасывающий исключение.

{ {

cout << "AClass: constructor." << endl;

cout << "Throwing exception..." << endl;

throw "Exception!";

}

~AClass() // Деструктор.

{

cout << "AClass: destructor."<< endl; }

void *operator new (size t size) // new класса.

{

cout<< "AClass: new." << endl;

return ::new char[size];

}

void operator delete(void *p) // delete класса.

{

cout << "AClass: delete." << endl;

::delete[] p;

}

};

int main() {

AClass *ap;

try {

ар = new AClassO; // Попытка выделить, объект.

}

catch(char *str) {

cout << "Caught a sring: " << str << endl;

)

return 0;

}

Эта программа выводит:

AClass: new.

AClass: constructor.

Throwing exception...

AClass: delete.

Caught a string: Exception!

Таким образом, при исключении память объекта освобождается операцией класса delete.

Классы исключений

Часто для обработки исключительных ситуаций используются классы, специально предназначенные для этой цели. Главное в управлении исключениями — отыскать нужный обработчик, а это делается путем сопоставления типа выброшенного объекта с типами, объявленными в обработчиках. Поэтому иногда для исключений определяют совершенно “пустые” классы, однако с уникальными именами.

Классы исключений программы могут быть организованы в иерархическую структуру. Схожие типы исключений объявляются в качестве производных одного и того же базового класса, являющегося их обобщением. Используя полиморфные механизмы, можно перехватывать только указатель или ссылку на базовый класс; полиморфизм обеспечит адекватную обработку исключения любого производного класса. Вот примерная схема:

class GenericFault { // Обобщенная ошибка.

public: virtual void Report ();

//

// Конкретные типы ошибок...

//

class OpenError: public GenericFault {

public:

void Report();

}

class BadHeader: public GenericFault { public:

void Report ();

class BadRecord: public GenericFault ( public:

void Report () ;

}

int main() {

try {

}

catch(GenericFault &err) { err.Report () ;

}

return 0;

}

Предопределенные исключения

В библиотеке C++ имеется несколько предопределенных классов исключений. В следующих параграфах мы рассмотрим некоторые из них.

xmsg

Класс xmsg предназначен для передачи строковых сообщений об исключениях. Он объявлен в заголовке except, h:

class EXPCLASS xmsg : public std::exception

{ public:

xmsg(const std::string &msg);

xmsg(const xmsg &);

virtual ~xmsg() throw ();

xmsg & operator=(const xmsg &) ;

virtual const char * what() const throw ();

const std::string & why() const;

void raise () throw(xmsg);

private:

std::string *str;

};

Применять класс xmsg очень просто:

#include <iostream.h>

#include <except.h>

#pragma hdrstop

#include <condefs.h>

int main() {

try {

xmsg X("Exception!");

throw X;

// или

//

X.raise () ;

}

catch(xmsg Smsg) {

cout << "Caught in main: " << msg.why() << end1;

}

return 0;

}

В классе имеется функция-элемент raise (), посредством которой объект выбрасывает сам себя. Ею можно воспользоваться вместо оператора throw. Функция why () возвращает стандартную строку с сообщением, записанным в объект.

Класс xmsg считается устаревшим. Теперь в стандартной библиотеке C++ определяется ряд классов (производных от exception, как и xmsg), организованных иерархически. По сути они ничем друг от друга не отличаются; данные им имена ничего особенного не значат. Вот эти классы: class logic_error public exception class domain_error public logic_error class invalid argument public logic_error class length_error public logic_error class out_of_range public logic_error class runtime error public exception class range error public runtime error class overflow_error public runtime error class underflow error public runtime error

Как видите, logic_error и runtime_error — производные от exception, а все остальные — от первых двух. Эти классы имеют конструктор, принимающий ссылку на стандартную строку, виртуальный деструктор и виртуальную функцию-элемент what (), которая возвращает указатель на константную строку С. Вот пример:

#include <iostream>

#include <stdexcept>

using namespace std;

static void f() ( throw runtime_error("a runtime error");

}

int main ()

{

try

{ f();

}

catch (const exceptions e) {

cout << "Got an exception: " << e.what() << endl;

} return 0;

}

Программа печатает:

Got an exception: a runtime error

Иерархия и полиморфизм этих классов делают их весьма гибким средством организации обработки ошибок.

bad_alloc

Если операция new не может выделить запрашиваемую память, она выбрасывает исключение bad_alloc. Этот класс также является производным от exception:

class bad alloc : public exception {

public:

bad_alloc () throw() : exception () { ; }

bad_alloc(const bad_alioc&) throw()

{ ; }

bad_alloc& operator=(const bad_alloc&) throw()

{ return *this; }

virtual ~bad_alloc () throw ();

virtual const char * what () const throw()

{

return _RWSTD::_rw_stdexcept_BadAilocException;

} };

Продемонстрировать реальную работу этого класса с реальной операцией new вряд ли возможно, да и не стоит. Можно, однако, выбросить bad_alloc вручную:

int main () {

try {

throw bad_alloc();

} catch(const exception &e) (

cout << "Caught something: "<< e.what() “ endl;

}

return 0;

}

Функция what () объекта bad_alloc возвращает строку "bad alloc exception thrown".

В прежних версиях языка new при ошибке выбрасывала xalloc, класс, производный от xmsg:

class _EXPCLASS xalloc : public xmsg

{

public:

xalloc(const std::string &msg, _SIZE_T size);

_SIZE_T requested () const.;

void raise () throw(xalloc);

private:

_SIZE_T siz;

};

Теперь этим классом пользоваться нельзя, и он остался в библиотеке C++Builder только для обеспечения совместимости со старыми библиотеками. Еще раньше операция new при отказе просто возвращала NULL. Такое поведение операции восстановить очень просто, вызвав функцию set_new_hand-ler():

#include <new.h>

set new handler (0);

Прототип set_new_handler () имеет вид

typedef void (*new_handler)();

new_handler set__new_handler(new_handler rny_handler);

Функция предназначена для установки пользовательского обработчика ошибок глобальных операций new или new [ ]. Обработчик my handler может:

В идеале обработчик освобождает ненужную память и возвращает управление. В этом случае new снова попытается удовлетворить запрос.

В следующей главе мы встретимся еще с двумя предопределенными классами исключений — bad_typeid и bad_ typeinfo.

Информация об исключении

Имеются три глобальные переменные, в которых хранится информация о текущем исключении (они объявлены в заголовке except, h):

Чтобы эта информация стала доступной, на странице C++ диалога Project Options нужно установить флажок Location information в группе Exception handling (по умолчанию выключен). Он соответствует ключу командной строки -хр.

Вот пример:

//////////////////////////////////////

// Loclnfo.cpp: Информация о точке выброса исключения.

//

#include <iostream.h>

#include <stdexcept>

#pragma hdrstop

#include <condefs.h>

void f() {

throw invalid_argument("Exception from f(): ");

}

int main() {

try { f () ;

}

catch(const exception &e) { cout << e.what()

<< _throwExceptionName << end1

<< " in file " << _throwFileName << end1

<< " line " << _throwLineNumber << end1;

}

return 0; }

Программа печатает:

Exception from f(): invalid argument

in file C:\Projects\Chl2\LocInfo\LocInfo.cpp line 8

Установки компилятора

Помимо упоминавшегося выше флажка Location information, в группе Exception handling страницы C++ диалога установок проекта находятся следующие флажки:

О системных исключениях

Системные или процессорные исключения вроде деления на ноль невозможно обработать, пользуясь только механизмом исключений языка C++. Эти исключения сразу перехватываются операционной системой. Тем не менее, C++Builder поддерживает уже упоминавшееся в 4-й главе структурированное управление исключениями (SEH — Structured Exception Handling}, реализованное первоначально в качестве интегрированной части Windows NT и позволяющее работать с процессорными исключениями.

SEH в C++

Как вы, возможно, помните, в языке С (расширенном) имеются две структуры управления исключениями. Это обработка кадра (__try/_except) и обработка завершения (_try/_finally).

Обработки завершения мы здесь касаться не будем и вообще не будем в подробностях рассматривать работу со структурированными исключениями в C++, поскольку нас прежде всего интересуют здесь системные исключения, не поддерживаемые стандартным C++. Вместо ключевого слова _try в коде C++ принято писать просто try, как и для встроенных исключений языка. Таким образом, мы будем пользоваться только конструкцией try/_except.

Итак, синтаксис кадровой структуры управления имеет вид:

try

{

<тело защищенного блока> }

_except(<выражение-фильтр>) {

<блок обработки кадра>. }

В программе можно одновременно использовать управление исключениями C++ и кадрированную обработку исключений. Их структуры могут быть вложенными.

Фильтры

Фильтрующее выражение должно принимать одно из трех значений:

EXCEPTION_EXECUTE_HANDLER

EXCEPTION_CONTINUE_SEARCH

EXCEPTION_CONTINUE_EXECUTION

Они соответствуют исполнению данного обработчика, продолжению поиска (стек разматывается, и поиск обработчика переходит на более высокий уровень) и продолжению выполнения программы с той точки, где было возбуждено исключение.

В выражении фильтра можно вызвать функцию GetExceptionInformation() или GetExceptionCode () . Первая из них возвращает указатель на структуру EXCEPTION_POINTERS, которая содержит детальную ин-

формацию об исключении; вторая возвращает только кодовое значение для исключения. Чаще всего в качестве выражения фильтра используют функцию, аргументом которой является значение, возвращаемое одной из этих двух функций.

Сама функция фильтра не может вызывать GetExceptionInformation () или GetExceptionCode (). Значения, возвращаемые этими функциями, должны передаваться ей в аргументах.

Точно так же и блок обработчика не может вызывать данные функции. Функция фильтра должна скопировать необходимую информацию в такое место, где она будет доступна для обработчика исключения.

Вот пример простейшей функции фильтра:

int MyFilter(int code) {

if (code == EXCEPTION_ACCESS_VIOLATION) return EXCEPTION_EXECUTE_HANDLER;

else

return EXCEPTION_CONTINUE_SEARCH;

}

try {

}

_except(MyFilter(GetExceptionCode())) {

}

Процессорные исключения

Наибольший интерес для нас представляют исключения, связанные с процессором. Их, как уже говорилось, нельзя обрабатывать стандартными средствами C++. Для этих исключений в заголовке winbase.h определен ряд символических констант. Вот некоторые из них:

EXCEPTION_ACCESS_VIOLATION

EXCEPTION_ARRAY_BOUNDS_EXCEEDED

EXCEPTION_FLT_DENORMAL_OPERAND

EXCEPTION_FLT_DIVIDE_BY_ZERO

EXCEPTION_FLT_INEXACT_RESULT

EXCEPTION_FLT_INVALID_OPERATION

EXCEPTION_FLT_OVERFLOW

EXCEPTION_FLT_STACK_CHECK

EXCEPTION_FLT_UNDERFLOW

EXCEPTION_INT_DIVIDE_BY_ZERO

EXCEPTION_INT_OVERFLOW

EXCEPTION_PRIV_INSTRUCTION

EXCEPT ION_IN_PAGE_ERROR

EXCEPTION_ILLEGAL_INSTRUCTION

EXCEPTION_NONCONTINUABLE_EXCEPTION

EXCEPTION_STACK_OVERFLOW

EXCEPTION_INVALID_DISPOSITION

EXCEPTION_GUARD_PAGE

Ниже показан пример, в котором имитируется нарушение доступа путем разыменования нулевого указателя. Функция фильтра детектирует эту ошибку, и обработчик исключения выводит соответствующее собщение:

/////////////////////////////////

// Access.cpp: Применение SEH для перехвата

// системных исключений.

//

#include <except.h>

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

static int xfilter(EXCEPTION_POINTERS *info)

{

if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)

return EXCEPTION_EXECUTE_HANDLER;

else

return EXCEPTION_CONTINUE_SEARCH;

}

int main () {

try (

int *p = NULL;

*P = -1;

}

_except(xfilter(GetExceptionInformation())) { cerr << "Exception Access Violaton caught..." << endl;

exit(l);

} cout<<"Normal exit..." << endl;

return 0;

}

Для справки приведем описание структуры exception_pointers:

struct EXCEPTION_POINTERS {

EXCEPTION_RECORD *ExceptionRecord;

CONTEXT *Context;

};

Struct EXCEPTION_RECORD { DWORD ExceptionCode;

DWORD ExceptionFlags;

struct EXCEPTION_RECORD *ExceptionRecord;

void *ExceptionAddress;

DWORD NumberParameters;

DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

Как уже говорилось, структуры управления исключениями SEH и C++ могут быть вложены друг в друга. Более того, except-обработчик может выбрасывать исключение C++, которое будет далее перехватываться catch-обработчиком. Можно в целях удобства и единообразия заключить все критические участки кода, могущие возбудить процессорные исключения (или вообще всю программу) в блоки try/ except, выбрасывающие исключения C++, и затем обрабатывать их наравне с другими исключениями. Рассмотрите такой пример:

////////////////////////////////

// SehPlus.cpp: Переход от SEH к C++.

//

#include <iostream.h>

#include <excpt.h>

#include <stdexcept>

#pragma hdrstop

#include <condefs.h>

static EXCEPTIONJRECORD eRec;

static int xfliter(EXCEPTION_POINTERS *xp) {

eRec = *(xp->ExceptionRecord);

return EXCEPTION_EXECUTE_HANDLER;

}

int main () {

double d = 10000;

try { try {

for (int i=5; i>=0; i-) { d = d / i;

cout << i << "... ";

} }

_except- ixfliter(GetExceptionInformation ())) ( if (eRec.ExceptionCode ==

EXCEPTIOM_FLT_DIVIDE_BY_ZERO) throw runtime error(

"Floating point divide by zero!");

else

throw runtime_error(

"Unknown processor exception.");

} }

catch(const exception &e) { cout << e.what() << end1;

} return 0;

}

Программа выводит:

5... 4... 3...2... I... Floating point divide by zero!

Целесообразно было бы предусмотреть для процессорных исключений специальный класс, производный от, например, runtime_error, дополнив его структурой EXCEPTION RECORD. Тогда вообще вся обработка осуществлялась бы средствами C++.

Заключение

В этой главе вы познакомились со средствами C++, позволяющими сделать обработку ошибок и других исключительных ситуаций гораздо более единообразной и надежной, чем это было возможно когда-либо прежде. Кроме того, исключения используются стандартными библиотеками и в самом языке (класс string, операция new). Поэтому я всячески призываю вас практиковаться и привыкать к управлению исключениями.